Kuasai otomatisasi email dengan imaplib Python. Panduan mendalam ini mencakup koneksi ke server IMAP, pencarian, pengambilan, penguraian email, penanganan lampiran, dan manajemen kotak surat layaknya profesional.
Klien IMAP Python: Panduan Lengkap untuk Pengambilan Email dan Manajemen Kotak Surat
Email tetap menjadi landasan komunikasi digital bagi bisnis dan individu di seluruh dunia. Namun, mengelola volume email yang tinggi bisa menjadi tugas yang memakan waktu dan berulang. Mulai dari memproses faktur dan memfilter notifikasi hingga mengarsipkan percakapan penting, upaya manual dapat dengan cepat menjadi berlebihan. Di sinilah otomatisasi terprogram bersinar, dan Python, dengan pustaka standarnya yang kaya, menyediakan alat yang ampuh untuk mengendalikan kotak masuk Anda.
Panduan lengkap ini akan memandu Anda melalui proses membangun klien IMAP Python dari awal menggunakan pustaka bawaan imaplib
. Anda akan belajar tidak hanya cara mengambil email, tetapi juga cara mengurai kontennya, mengunduh lampiran, dan mengelola kotak surat Anda dengan menandai pesan sebagai sudah dibaca, memindahkannya, atau menghapusnya. Di akhir artikel ini, Anda akan siap untuk mengotomatiskan tugas email Anda yang paling membosankan, menghemat waktu dan meningkatkan produktivitas Anda.
Memahami Protokol: IMAP vs. POP3 vs. SMTP
Sebelum menyelami kode, penting untuk memahami protokol dasar yang mengatur email. Anda akan sering mendengar tiga akronim: SMTP, POP3, dan IMAP. Masing-masing melayani tujuan yang berbeda.
- SMTP (Simple Mail Transfer Protocol): Ini adalah protokol untuk mengirim email. Anggap SMTP sebagai layanan pos yang mengambil surat Anda dan mengirimkannya ke server kotak surat penerima. Saat skrip Python Anda mengirim email, ia menggunakan SMTP.
- POP3 (Post Office Protocol 3): Ini adalah protokol untuk mengambil email. POP3 dirancang untuk terhubung ke server, mengunduh semua pesan baru ke klien lokal Anda, dan kemudian, secara default, menghapusnya dari server. Ini seperti pergi ke kantor pos, mengumpulkan semua surat Anda, dan membawanya pulang; setelah berada di rumah Anda, surat itu tidak lagi ada di kantor pos. Model ini kurang umum saat ini karena keterbatasannya di dunia multi-perangkat.
- IMAP (Internet Message Access Protocol): Ini adalah protokol modern untuk mengakses dan mengelola email. Berbeda dengan POP3, IMAP membiarkan pesan di server dan menyinkronkan status (dibaca, belum dibaca, ditandai, dihapus) di semua klien yang terhubung. Saat Anda membaca email di ponsel Anda, email itu muncul sebagai sudah dibaca di laptop Anda. Model yang berpusat pada server ini sangat cocok untuk otomatisasi karena skrip Anda dapat berinteraksi dengan kotak surat sebagai klien lain, dan perubahan yang dilakukannya akan tercermin di mana saja. Untuk panduan ini, kami akan fokus secara eksklusif pada IMAP.
Memulai dengan imaplib
Python
Pustaka standar Python menyertakan imaplib
, sebuah modul yang menyediakan semua alat yang diperlukan untuk berkomunikasi dengan server IMAP. Tidak diperlukan paket eksternal untuk memulai.
Prasyarat
- Python Terinstal: Pastikan Anda telah menginstal versi Python terbaru (3.6 atau lebih baru) di sistem Anda.
- Akun Email dengan IMAP Diaktifkan: Sebagian besar penyedia email modern (Gmail, Outlook, Yahoo, dll.) mendukung IMAP. Anda mungkin perlu mengaktifkannya di pengaturan akun Anda.
Keamanan Dahulu: Gunakan Kata Sandi Aplikasi, Bukan Kata Sandi Utama Anda
Ini adalah langkah paling penting untuk keamanan. Jangan menyematkan kata sandi akun email utama Anda ke dalam skrip Anda. Jika kode Anda pernah terkompromi, seluruh akun Anda berisiko. Sebagian besar penyedia email utama yang menggunakan Otentikasi Dua Faktor (2FA) mengharuskan Anda untuk membuat "Kata Sandi Aplikasi".
Kata Sandi Aplikasi adalah kode sandi unik 16 digit yang memberikan izin kepada aplikasi tertentu untuk mengakses akun Anda tanpa memerlukan kata sandi utama atau kode 2FA Anda. Anda dapat membuatnya dan mencabutnya kapan saja tanpa memengaruhi kata sandi utama Anda.
- Untuk Gmail: Buka pengaturan Akun Google Anda -> Keamanan -> Verifikasi 2 Langkah -> Kata Sandi Aplikasi.
- Untuk Outlook/Microsoft: Buka dasbor keamanan Akun Microsoft Anda -> Opsi keamanan lanjutan -> Kata sandi Aplikasi.
- Untuk penyedia lain: Cari dokumentasi mereka untuk "kata sandi aplikasi" atau "kata sandi khusus aplikasi".
Setelah dibuat, perlakukan Kata Sandi Aplikasi ini seperti kredensial lainnya. Praktik terbaik adalah menyimpannya di variabel lingkungan atau sistem manajemen rahasia yang aman daripada langsung di kode sumber Anda.
Koneksi Dasar
Mari kita tulis kode pertama kita untuk membuat koneksi aman ke server IMAP, masuk, lalu keluar dengan anggun. Kita akan menggunakan imaplib.IMAP4_SSL
untuk memastikan koneksi kita terenkripsi.
import imaplib
import os
# --- Kredensial ---
# Sebaiknya muat ini dari variabel lingkungan atau file konfigurasi
# Untuk contoh ini, kita akan mendefinisikannya di sini. Ganti dengan detail Anda.
EMAIL_ACCOUNT = "your_email@example.com"
APP_PASSWORD = "your_16_digit_app_password"
IMAP_SERVER = "imap.example.com" # misal, "imap.gmail.com"
# --- Hubungkan ke server IMAP ---
# Kami menggunakan blok try...finally untuk memastikan kami logout dengan anggun
conn = None
try:
# Hubungkan menggunakan SSL untuk koneksi yang aman
conn = imaplib.IMAP4_SSL(IMAP_SERVER)
# Masuk ke akun
status, messages = conn.login(EMAIL_ACCOUNT, APP_PASSWORD)
if status == 'OK':
print("Berhasil masuk!")
# Kita akan menambahkan logika lebih lanjut di sini nanti
else:
print(f"Login gagal: {messages}")
finally:
if conn:
# Selalu logout dan tutup koneksi
conn.logout()
print("Sudah logout dan koneksi ditutup.")
Skrip ini membangun fondasi. Blok try...finally
sangat penting karena menjamin bahwa conn.logout()
dipanggil, menutup sesi dengan server, bahkan jika terjadi kesalahan selama operasi kita.
Menavigasi Kotak Surat Anda
Setelah masuk, Anda dapat mulai berinteraksi dengan kotak surat (sering disebut folder) di akun Anda.
Mencantumkan Semua Kotak Surat
Untuk melihat kotak surat yang tersedia, Anda dapat menggunakan metode conn.list()
. Outputnya bisa sedikit berantakan, jadi sedikit penguraian diperlukan untuk mendapatkan daftar nama yang bersih.
# Di dalam blok 'try' setelah login berhasil:
status, mailbox_list = conn.list()
if status == 'OK':
print("Kotak Surat Tersedia:")
for mailbox in mailbox_list:
# Entri kotak surat mentah adalah string byte yang perlu didekode
# Seringkali diformat seperti: (\HasNoChildren) "/" "INBOX"
# Kita bisa melakukan penguraian dasar untuk membersihkannya
parts = mailbox.decode().split(' "/" ')
if len(parts) == 2:
mailbox_name = parts[1].strip('"')
print(f"- {mailbox_name}")
Ini akan mencetak daftar seperti 'INBOX', 'Sent', '[Gmail]/Spam', dll., tergantung pada penyedia email Anda.
Memilih Kotak Surat
Sebelum Anda dapat mencari atau mengambil email, Anda harus memilih kotak surat untuk dikerjakan. Pilihan yang paling umum adalah 'INBOX'. Metode conn.select()
mengaktifkan kotak surat. Anda juga dapat membukanya dalam mode hanya-baca jika Anda tidak bermaksud membuat perubahan (seperti menandai email sebagai sudah dibaca).
# Pilih 'INBOX' untuk dikerjakan.
# Gunakan readonly=True jika Anda tidak ingin mengubah flag email (misalnya, dari UNSEEN ke SEEN)
status, messages = conn.select('INBOX', readonly=False)
if status == 'OK':
total_messages = int(messages[0])
print(f"INBOX dipilih. Total pesan: {total_messages}")
else:
print(f"Gagal memilih INBOX: {messages}")
Saat Anda memilih kotak surat, server mengembalikan jumlah total pesan yang dikandungnya. Semua perintah selanjutnya untuk mencari dan mengambil akan berlaku untuk kotak surat yang dipilih ini.
Mencari dan Mengambil Email
Ini adalah inti dari pengambilan email. Prosesnya melibatkan dua langkah: pertama, mencari pesan yang cocok dengan kriteria tertentu untuk mendapatkan ID uniknya, dan kedua, mengambil konten pesan tersebut menggunakan ID-nya.
Kekuatan search()
Metode search()
sangat serbaguna. Ini tidak mengembalikan email itu sendiri, melainkan daftar nomor urut pesan (ID) yang cocok dengan kueri Anda. ID ini spesifik untuk sesi saat ini dan kotak surat yang dipilih.
Berikut adalah beberapa kriteria pencarian yang paling umum:
'ALL'
: Semua pesan di kotak surat.'UNSEEN'
: Pesan yang belum dibaca.'SEEN'
: Pesan yang sudah dibaca.'FROM "sender@example.com"'
: Pesan dari pengirim tertentu.'TO "recipient@example.com"'
: Pesan yang dikirim ke penerima tertentu.'SUBJECT "Your Subject Line"'
: Pesan dengan subjek tertentu.'BODY "a keyword in the body"'
: Pesan yang berisi string tertentu di badan.'SINCE "01-Jan-2024"'
: Pesan yang diterima pada atau setelah tanggal tertentu.'BEFORE "31-Jan-2024"'
: Pesan yang diterima sebelum tanggal tertentu.
Anda juga dapat menggabungkan kriteria. Misalnya, untuk menemukan semua email yang belum dibaca dari pengirim tertentu dengan subjek tertentu, Anda akan mencari '(UNSEEN FROM "alerts@example.com" SUBJECT "System Alert")'
.
Mari kita lihat aksinya:
# Cari semua email yang belum dibaca di INBOX
status, message_ids = conn.search(None, 'UNSEEN')
if status == 'OK':
# message_ids adalah daftar string byte, misal, [b'1 2 3']
# Kita perlu membaginya menjadi ID individu
email_id_list = message_ids[0].split()
if email_id_list:
print(f"Ditemukan {len(email_id_list)} email yang belum dibaca.")
else:
print("Tidak ditemukan email yang belum dibaca.")
else:
print("Pencarian gagal.")
Mengambil Konten Email dengan fetch()
Sekarang Anda memiliki ID pesan, Anda dapat menggunakan metode fetch()
untuk mengambil data email yang sebenarnya. Anda perlu menentukan bagian email mana yang Anda inginkan.
'RFC822'
: Ini mengambil seluruh konten email mentah, termasuk semua header dan bagian badan. Ini adalah opsi yang paling umum dan komprehensif.'BODY[]'
: Sinonim untukRFC822
.'ENVELOPE'
: Mengambil informasi header utama seperti Tanggal, Subjek, Dari, Ke, dan Dalam-Balasan-Ke. Ini lebih cepat jika Anda hanya memerlukan metadata.'BODY[HEADER]'
: Hanya mengambil header.
Mari kita ambil konten lengkap dari email yang belum dibaca pertama yang kita temukan:
if email_id_list:
first_email_id = email_id_list[0]
# Ambil data email untuk ID yang diberikan
# 'RFC822' adalah standar yang menentukan format pesan teks
status, msg_data = conn.fetch(first_email_id, '(RFC822)')
if status == 'OK':
for response_part in msg_data:
# Perintah fetch mengembalikan tuple, di mana bagian kedua adalah konten email
if isinstance(response_part, tuple):
raw_email = response_part[1]
# Sekarang kita memiliki data email mentah dalam bentuk byte
# Langkah selanjutnya adalah mengurainya
print("Berhasil mengambil email.")
# Kita akan memproses `raw_email` di bagian berikutnya
else:
print("Fetch gagal.")
Mengurai Konten Email dengan Modul email
Data mentah yang dikembalikan oleh fetch()
adalah string byte yang diformat sesuai dengan standar RFC 822. Ini tidak mudah dibaca. Modul email
bawaan Python dirancang khusus untuk mengurai pesan mentah ini menjadi struktur objek yang mudah digunakan.
Membuat Objek Message
Langkah pertama adalah mengonversi string byte mentah menjadi objek Message
menggunakan email.message_from_bytes()
.
import email
from email.header import decode_header
# Asumsikan `raw_email` berisi data byte dari perintah fetch
email_message = email.message_from_bytes(raw_email)
Mengekstrak Informasi Utama (Header)
Setelah Anda memiliki objek Message
, Anda dapat mengakses headernya seperti kamus.
# Dapatkan subjek, dari, ke, dan tanggal
subject = email_message["Subject"]
from_ = email_message["From"]
to_ = email_message["To"]
date_ = email_message["Date"]
# Header email dapat berisi karakter non-ASCII, jadi kita perlu mendekodenya
def decode_email_header(header):
decoded_parts = decode_header(header)
header_str = ""
for part, encoding in decoded_parts:
if isinstance(part, bytes):
# Jika ada pengkodean, gunakan itu. Jika tidak, default ke utf-8.
header_str += part.decode(encoding or 'utf-8')
else:
header_str += part
return header_str
subject = decode_email_header(subject)
from_ = decode_email_header(from_)
print(f"Subjek: {subject}")
print(f"Dari: {from_}")
Fungsi pembantu decode_email_header
penting karena header sering kali dikodekan untuk menangani set karakter internasional. Hanya mengakses email_message["Subject"]
mungkin memberi Anda string dengan urutan karakter yang membingungkan jika Anda tidak mendekodenya dengan benar.
Menangani Badan Email dan Lampiran
Email modern sering kali "multipart", yang berarti mereka berisi versi konten yang berbeda (seperti teks biasa dan HTML) dan mungkin juga menyertakan lampiran. Kita perlu menelusuri bagian-bagian ini untuk menemukan apa yang kita cari.
Metode msg.is_multipart()
memberi tahu kita apakah email memiliki banyak bagian, dan msg.walk()
menyediakan cara mudah untuk mengulanginya.
def process_email_body(msg):
body = ""
attachments = []
if msg.is_multipart():
# Ulangi bagian email
for part in msg.walk():
content_type = part.get_content_type()
content_disposition = str(part.get("Content-Disposition"))
try:
# Dapatkan badan email
if content_type == "text/plain" and "attachment" not in content_disposition:
payload = part.get_payload(decode=True)
charset = part.get_content_charset() or 'utf-8'
body = payload.decode(charset)
# Dapatkan lampiran
elif "attachment" in content_disposition:
filename = part.get_filename()
if filename:
# Dekode nama file jika perlu
decoded_filename = decode_email_header(filename)
attachments.append({
'filename': decoded_filename,
'data': part.get_payload(decode=True)
})
except Exception as e:
print(f"Kesalahan memproses bagian: {e}")
else:
# Bukan pesan multipart, cukup dapatkan payload
payload = msg.get_payload(decode=True)
charset = msg.get_content_charset() or 'utf-8'
body = payload.decode(charset)
return body, attachments
# Menggunakan fungsi dengan pesan yang diambil
email_body, email_attachments = process_email_body(email_message)
print("\n--- Badan Email ---")
print(email_body)
if email_attachments:
print("\n--- Lampiran ---")
for att in email_attachments:
print(f"Nama File: {att['filename']}")
# Contoh menyimpan lampiran
with open(att['filename'], 'wb') as f:
f.write(att['data'])
print(f"Lampiran disimpan: {att['filename']}")
Fungsi ini secara cerdas membedakan antara badan teks biasa dan lampiran file dengan memeriksa header Content-Type
dan Content-Disposition
dari setiap bagian.
Manajemen Kotak Surat Tingkat Lanjut
Mengambil email hanyalah setengah dari perjuangan. Otomatisasi sejati melibatkan pengubahan status pesan di server. Perintah store()
adalah alat utama Anda untuk ini.
Menandai Email (Dibaca, Belum Dibaca, Ditandai)
Anda dapat menambah, menghapus, atau mengganti flag pada pesan. Flag yang paling umum adalah \Seen
, yang mengontrol status baca/belum dibaca.
- Tandai sebagai Dibaca:
conn.store(msg_id, '+FLAGS', '\Seen')
- Tandai sebagai Belum Dibaca:
conn.store(msg_id, '-FLAGS', '\Seen')
- Tandai/Bintangi Email:
conn.store(msg_id, '+FLAGS', '\Flagged')
- Batal Tandai Email:
conn.store(msg_id, '-FLAGS', '\Flagged')
Menyalin dan Memindahkan Email
Tidak ada perintah "pindahkan" langsung di IMAP. Memindahkan email adalah proses dua langkah:
- Salin pesan ke kotak surat tujuan menggunakan
conn.copy()
. - Tandai pesan asli untuk dihapus menggunakan flag
\Deleted
.
# Asumsikan `msg_id` adalah ID email yang akan dipindahkan
# 1. Salin ke kotak surat 'Archive'
status, _ = conn.copy(msg_id, 'Archive')
if status == 'OK':
print(f"Pesan {msg_id.decode()} disalin ke Archive.")
# 2. Tandai yang asli untuk dihapus
conn.store(msg_id, '+FLAGS', '\Deleted')
print(f"Pesan {msg_id.decode()} ditandai untuk dihapus.")
Menghapus Email Secara Permanen
Menandai pesan dengan \Deleted
tidak segera menghapusnya. Ini hanya menyembunyikannya dari tampilan di sebagian besar klien email. Untuk menghapus secara permanen semua pesan di kotak surat yang dipilih saat ini yang ditandai untuk dihapus, Anda harus memanggil metode expunge()
.
Peringatan: expunge()
tidak dapat diubah. Setelah dipanggil, data akan hilang selamanya.
# Ini akan menghapus secara permanen semua pesan dengan flag \Deleted
status, response = conn.expunge()
if status == 'OK':
print(f"{len(response)} pesan dihapus (dihapus secara permanen).")
Efek samping penting dari expunge()
adalah dapat memberi nomor ulang ID pesan untuk semua pesan selanjutnya di kotak surat. Karena alasan ini, sebaiknya identifikasi semua pesan yang ingin Anda proses, lakukan tindakan Anda (seperti menyalin dan menandai untuk dihapus), dan kemudian panggil expunge()
sekali di akhir sesi Anda.
Menyatukan Semuanya: Contoh Praktis
Mari buat skrip lengkap yang melakukan tugas dunia nyata: Pindai kotak masuk untuk email yang belum dibaca dari "invoices@mycorp.com", unduh lampiran PDF apa pun, dan pindahkan email yang diproses ke kotak surat bernama "Processed-Invoices".
import imaplib
import email
from email.header import decode_header
import os
# --- Konfigurasi ---
EMAIL_ACCOUNT = "your_email@example.com"
APP_PASSWORD = "your_16_digit_app_password"
IMAP_SERVER = "imap.gmail.com"
TARGET_SENDER = "invoices@mycorp.com"
DESTINATION_MAILBOX = "Processed-Invoices"
DOWNLOAD_DIR = "invoices"
# Buat direktori unduhan jika belum ada
if not os.path.isdir(DOWNLOAD_DIR):
os.mkdir(DOWNLOAD_DIR)
def decode_email_header(header):
# (Fungsi yang sama seperti yang didefinisikan sebelumnya)
decoded_parts = decode_header(header)
header_str = ""
for part, encoding in decoded_parts:
if isinstance(part, bytes):
header_str += part.decode(encoding or 'utf-8')
else:
header_str += part
return header_str
conn = None
try:
# --- Hubungkan dan Masuk ---
conn = imaplib.IMAP4_SSL(IMAP_SERVER)
conn.login(EMAIL_ACCOUNT, APP_PASSWORD)
print("Login berhasil.")
# --- Pilih INBOX ---
conn.select('INBOX')
print("INBOX dipilih.")
# --- Cari email ---
search_criteria = f'(UNSEEN FROM "{TARGET_SENDER}")'
status, message_ids = conn.search(None, search_criteria)
if status != 'OK':
raise Exception("Pencarian gagal")
email_id_list = message_ids[0].split()
if not email_id_list:
print("Tidak ditemukan faktur baru.")
else:
print(f"Ditemukan {len(email_id_list)} faktur baru untuk diproses.")
# --- Proses Setiap Email ---
for email_id in email_id_list:
print(f"\nMemproses ID email: {email_id.decode()}")
# Ambil email
status, msg_data = conn.fetch(email_id, '(RFC822)')
if status != 'OK':
print(f"Gagal mengambil email ID {email_id.decode()}")
continue
raw_email = msg_data[0][1]
email_message = email.message_from_bytes(raw_email)
subject = decode_email_header(email_message["Subject"])
print(f" Subjek: {subject}")
# Cari lampiran
for part in email_message.walk():
if part.get_content_maintype() == 'multipart':
continue
if part.get('Content-Disposition') is None:
continue
filename = part.get_filename()
if filename and filename.lower().endswith('.pdf'):
decoded_filename = decode_email_header(filename)
filepath = os.path.join(DOWNLOAD_DIR, decoded_filename)
# Simpan lampiran
with open(filepath, 'wb') as f:
f.write(part.get_payload(decode=True))
print(f" -> Lampiran diunduh: {decoded_filename}")
# --- Pindahkan email yang diproses ---
# 1. Salin ke kotak surat tujuan
status, _ = conn.copy(email_id, DESTINATION_MAILBOX)
if status == 'OK':
# 2. Tandai yang asli untuk dihapus
conn.store(email_id, '+FLAGS', '\Deleted')
print(f" Email dipindahkan ke '{DESTINATION_MAILBOX}'.")
# --- Expunge dan Bersihkan ---
if email_id_list:
conn.expunge()
print("\nEmail yang dihapus sudah diexpunge.")
except Exception as e:
print(f"Terjadi kesalahan: {e}")
finally:
if conn:
conn.logout()
print("Sudah logout.")
Praktik Terbaik dan Penanganan Kesalahan
Saat membangun skrip otomatisasi yang kuat, pertimbangkan praktik terbaik berikut:
- Penanganan Kesalahan yang Kuat: Bungkus kode Anda dalam blok
try...except
untuk menangkap potensi masalah seperti kegagalan login (imaplib.IMAP4.error
), masalah jaringan, atau kesalahan penguraian. - Manajemen Konfigurasi: Jangan pernah menyematkan kredensial. Gunakan variabel lingkungan (
os.getenv()
), file konfigurasi (misalnya, INI atau YAML), atau manajer rahasia khusus. - Pencatatan: Alih-alih pernyataan
print()
, gunakan modullogging
Python. Ini memungkinkan Anda mengontrol verbositas output Anda, menulis ke file, dan menambahkan stempel waktu, yang sangat berharga untuk men-debug skrip yang berjalan tanpa pengawasan. - Pembatasan Tingkat: Jadilah warga internet yang baik. Jangan terlalu sering meminta server email. Jika Anda perlu memeriksa email baru secara teratur, pertimbangkan interval beberapa menit alih-alih detik.
- Pengkodean Karakter: Email adalah standar global, dan Anda akan menemui berbagai pengkodean karakter. Selalu coba tentukan charset dari bagian email (
part.get_content_charset()
) dan sediakan fallback (seperti 'utf-8') untuk menghindariUnicodeDecodeError
.
Kesimpulan
Anda sekarang telah melakukan perjalanan melalui seluruh siklus interaksi dengan server email menggunakan imaplib
Python. Kami telah membahas membuat koneksi aman, mencantumkan kotak surat, melakukan pencarian yang kuat, mengambil dan mengurai email multipart yang kompleks, mengunduh lampiran, dan mengelola status pesan di server.
Kekuatan pengetahuan ini sangat besar. Anda dapat membangun sistem untuk secara otomatis mengkategorikan tiket dukungan, mengurai data dari laporan harian, mengarsipkan buletin, memicu tindakan berdasarkan email peringatan, dan banyak lagi. Kotak masuk, yang dulunya merupakan sumber tenaga kerja manual, dapat menjadi sumber data yang kuat dan otomatis untuk aplikasi dan alur kerja Anda.
Tugas email apa yang akan Anda otomatiskan terlebih dahulu? Kemungkinannya terbatas hanya oleh imajinasi Anda. Mulailah dari yang kecil, bangun di atas contoh-contoh dalam panduan ini, dan dapatkan kembali waktu Anda dari kedalaman kotak masuk Anda.